// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/controls/rounded_scroll_bar.h"

#include <limits>

#include "ash/public/cpp/style/color_provider.h"
#include "base/bind.h"
#include "base/numerics/ranges.h"
#include "base/time/time.h"
#include "cc/paint/paint_flags.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/canvas.h"
#include "ui/views/controls/scrollbar/base_scroll_bar_thumb.h"

namespace ash {
namespace {

// Thickness of scroll bar thumb.
constexpr int kScrollThumbThicknessDp = 8;
// Radius of the scroll bar thumb.
constexpr int kScrollThumbRadiusDp = 4;
// How long for the scrollbar to hide after no scroll events have been received?
constexpr base::TimeDelta kScrollThumbHideTimeout = base::Milliseconds(500);
// How long for the scrollbar to fade away?
constexpr base::TimeDelta kScrollThumbFadeDuration = base::Milliseconds(240);
// Opacity values from go/semantic-color-system for "Scrollbar".
constexpr float kDefaultOpacity = 0.38f;
constexpr float kActiveOpacity = 1.0f;

}  // namespace

// A scroll bar "thumb" that paints itself with rounded ends.
class RoundedScrollBar::Thumb : public views::BaseScrollBarThumb {
 public:
  explicit Thumb(RoundedScrollBar* scroll_bar)
      : BaseScrollBarThumb(scroll_bar), scroll_bar_(scroll_bar) {}
  Thumb(const Thumb&) = delete;
  Thumb& operator=(const Thumb&) = delete;
  ~Thumb() override = default;

  // views::BaseScrollBarThumb:
  gfx::Size CalculatePreferredSize() const override {
    return gfx::Size(kScrollThumbThicknessDp, kScrollThumbThicknessDp);
  }

  void OnPaint(gfx::Canvas* canvas) override {
    cc::PaintFlags fill_flags;
    fill_flags.setStyle(cc::PaintFlags::kFill_Style);
    fill_flags.setAntiAlias(true);
    // May be null in tests.
    if (auto* color_provider = ColorProvider::Get()) {
      fill_flags.setColor(color_provider->GetContentLayerColor(
          ColorProvider::ContentLayerType::kScrollBarColor));
    }
    canvas->DrawRoundRect(GetLocalBounds(), kScrollThumbRadiusDp, fill_flags);
  }

  void OnStateChanged() override { scroll_bar_->OnThumbStateChanged(); }

 private:
  RoundedScrollBar* const scroll_bar_;
};

RoundedScrollBar::RoundedScrollBar(bool horizontal)
    : ScrollBar(horizontal),
      hide_scrollbar_timer_(
          FROM_HERE,
          kScrollThumbHideTimeout,
          base::BindRepeating(&RoundedScrollBar::HideScrollBar,
                              base::Unretained(this))) {
  // Moving the mouse directly into the thumb will also notify this view.
  SetNotifyEnterExitOnChild(true);

  auto* thumb = new Thumb(this);  // Owned by views hierarchy.
  SetThumb(thumb);
  thumb->SetPaintToLayer();
  thumb->layer()->SetFillsBoundsOpaquely(false);
  // The thumb is hidden by default.
  thumb->layer()->SetOpacity(0.f);
}

RoundedScrollBar::~RoundedScrollBar() = default;

void RoundedScrollBar::SetInsets(const gfx::Insets& insets) {
  insets_ = insets;
}

gfx::Rect RoundedScrollBar::GetTrackBounds() const {
  gfx::Rect bounds = GetLocalBounds();
  bounds.Inset(insets_);
  return bounds;
}

bool RoundedScrollBar::OverlapsContent() const {
  return true;
}

int RoundedScrollBar::GetThickness() const {
  // Extend the thickness by the insets on the sides of the bar.
  const int sides = IsHorizontal() ? insets_.top() + insets_.bottom()
                                   : insets_.left() + insets_.right();
  return kScrollThumbThicknessDp + sides;
}

void RoundedScrollBar::OnMouseEntered(const ui::MouseEvent& event) {
  ShowScrollbar();
}

void RoundedScrollBar::OnMouseExited(const ui::MouseEvent& event) {
  if (!hide_scrollbar_timer_.IsRunning())
    hide_scrollbar_timer_.Reset();
}

void RoundedScrollBar::ScrollToPosition(int position) {
  ShowScrollbar();
  views::ScrollBar::ScrollToPosition(position);
}

void RoundedScrollBar::ObserveScrollEvent(const ui::ScrollEvent& event) {
  // Scroll fling events are generated by moving a single finger over the
  // trackpad; do not show the scrollbar for these events.
  if (event.type() == ui::ET_SCROLL_FLING_CANCEL)
    return;
  ShowScrollbar();
}

views::BaseScrollBarThumb* RoundedScrollBar::GetThumbForTest() const {
  return GetThumb();
}

void RoundedScrollBar::ShowScrollbar() {
  if (!IsMouseHovered())
    hide_scrollbar_timer_.Reset();

  auto* thumb = GetThumb();
  const float target_opacity =
      thumb->IsMouseHovered() ? kActiveOpacity : kDefaultOpacity;
  if (base::IsApproximatelyEqual(thumb->layer()->GetTargetOpacity(),
                                 target_opacity,
                                 std::numeric_limits<float>::epsilon())) {
    return;
  }
  ui::ScopedLayerAnimationSettings animation(thumb->layer()->GetAnimator());
  animation.SetTransitionDuration(kScrollThumbFadeDuration);
  thumb->layer()->SetOpacity(target_opacity);
}

void RoundedScrollBar::HideScrollBar() {
  // Never hide the scrollbar if the mouse is over it. The auto-hide timer
  // will be reset when the mouse leaves the scrollable area.
  if (IsMouseHovered())
    return;

  hide_scrollbar_timer_.Stop();
  ui::ScopedLayerAnimationSettings animation(
      GetThumb()->layer()->GetAnimator());
  animation.SetTransitionDuration(kScrollThumbFadeDuration);
  GetThumb()->layer()->SetOpacity(0.f);
}

void RoundedScrollBar::OnThumbStateChanged() {
  // If the mouse is still in the scroll bar, the thumb hover state may have
  // changed, so recompute opacity.
  if (IsMouseHovered())
    ShowScrollbar();
}

BEGIN_METADATA(RoundedScrollBar, ScrollBar)
END_METADATA

}  // namespace ash
